9  Interactive interfaces

IMPORTANT: It appears that the quarto book format does not support interactive elements in the way that a normal .qmd compiled to html does. For that reason, the interactive elements in the following will not be displayed. For now, I leave this here as a useful reference - the code will still work outside of this book.

library(tidyverse)
library(gt) #tables and interactive tables
library(plotly) # interactive plots
library(trelliscopejs) # interactive version of facet_wrap
library(gapminder) #data to demonstrate trelliscopejs
library(gtExtras)

9.1 Overview

The FRAM team often needs to present relatively large sets of data to stake-holders who may be interested only in specific subsets of the data. By providing interactive interfaces, we add functionality to our presentations of the data as well as allowing users to customize the presentation to their needs.

9.1.1 Quick note on sharing interactive .html files

The tools described below are easier to implement locally (running in Rstudio on your own computer and exploring in the viewer) than including in a compiled in a quarto or Rmarkdown .html file. By default quarto and Rmarkdown will create html files that rely on additional files present on your local machine. The compiled document will look good on your computer, but when you share it with others they won’t be able to view the interactive figures or tables. This has definitely not been hugely frustrating.

We can fix this behavior by including two additional arguments in the YAML header of our Rmarkdown or quarto file: embed-resources: true as a sub-argument to format: html, and standalone: true as an independent argument. These setting produce a self-sufficient document that can be emailed or shared in Teams and viewed on other computers. Here’s the entire YAML header for a functional document.

title: "Presenting data interactively"
author: "Collin Edwards"
format: 
  html:
  embed-resources: true
standalone: true

9.2 Tables

We can use the gt package to generate and customize tables. See https://gt.rstudio.com/articles/gt.html for an introduction to all the customization options. We’ll use the built-in mtcars data for this example, adding in an explicit column for the car names (mtcars uses rownames for that), and we’ll keep the table formatting to the default.

9.2.1 Non-interactive table

We can quickly make a fairly pretty table with gt(). There are many options to improve readability and customize the figures, including adding headers, grouping sets of columns together with “spanner” labels, and adding footnotes. Footnotes in particular seem great, as we can use them to clarify column names within the table itself (rather than relying on users reading associated text in the main body of a document).

dat <- mtcars |> 
  mutate(car = rownames(mtcars)) |> 
  relocate(car, .before = "mpg")
dat |> 
  gt()
car mpg cyl disp hp drat wt qsec vs am gear carb
Mazda RX4 21.0 6 160.0 110 3.90 2.620 16.46 0 1 4 4
Mazda RX4 Wag 21.0 6 160.0 110 3.90 2.875 17.02 0 1 4 4
Datsun 710 22.8 4 108.0 93 3.85 2.320 18.61 1 1 4 1
Hornet 4 Drive 21.4 6 258.0 110 3.08 3.215 19.44 1 0 3 1
Hornet Sportabout 18.7 8 360.0 175 3.15 3.440 17.02 0 0 3 2
Valiant 18.1 6 225.0 105 2.76 3.460 20.22 1 0 3 1
Duster 360 14.3 8 360.0 245 3.21 3.570 15.84 0 0 3 4
Merc 240D 24.4 4 146.7 62 3.69 3.190 20.00 1 0 4 2
Merc 230 22.8 4 140.8 95 3.92 3.150 22.90 1 0 4 2
Merc 280 19.2 6 167.6 123 3.92 3.440 18.30 1 0 4 4
Merc 280C 17.8 6 167.6 123 3.92 3.440 18.90 1 0 4 4
Merc 450SE 16.4 8 275.8 180 3.07 4.070 17.40 0 0 3 3
Merc 450SL 17.3 8 275.8 180 3.07 3.730 17.60 0 0 3 3
Merc 450SLC 15.2 8 275.8 180 3.07 3.780 18.00 0 0 3 3
Cadillac Fleetwood 10.4 8 472.0 205 2.93 5.250 17.98 0 0 3 4
Lincoln Continental 10.4 8 460.0 215 3.00 5.424 17.82 0 0 3 4
Chrysler Imperial 14.7 8 440.0 230 3.23 5.345 17.42 0 0 3 4
Fiat 128 32.4 4 78.7 66 4.08 2.200 19.47 1 1 4 1
Honda Civic 30.4 4 75.7 52 4.93 1.615 18.52 1 1 4 2
Toyota Corolla 33.9 4 71.1 65 4.22 1.835 19.90 1 1 4 1
Toyota Corona 21.5 4 120.1 97 3.70 2.465 20.01 1 0 3 1
Dodge Challenger 15.5 8 318.0 150 2.76 3.520 16.87 0 0 3 2
AMC Javelin 15.2 8 304.0 150 3.15 3.435 17.30 0 0 3 2
Camaro Z28 13.3 8 350.0 245 3.73 3.840 15.41 0 0 3 4
Pontiac Firebird 19.2 8 400.0 175 3.08 3.845 17.05 0 0 3 2
Fiat X1-9 27.3 4 79.0 66 4.08 1.935 18.90 1 1 4 1
Porsche 914-2 26.0 4 120.3 91 4.43 2.140 16.70 0 1 5 2
Lotus Europa 30.4 4 95.1 113 3.77 1.513 16.90 1 1 5 2
Ford Pantera L 15.8 8 351.0 264 4.22 3.170 14.50 0 1 5 4
Ferrari Dino 19.7 6 145.0 175 3.62 2.770 15.50 0 1 5 6
Maserati Bora 15.0 8 301.0 335 3.54 3.570 14.60 0 1 5 8
Volvo 142E 21.4 4 121.0 109 4.11 2.780 18.60 1 1 4 2

9.2.2 Interactive table

The static table above works – and we can make it prettier with additional options – but the table is long and it can be clunky to compare individual cars or look for all the cars with the highest horsepower. Interactive tables provide solutions for this!

We can make a gt table interactive by piping it into the opt_interactive() function. opt_interactive() has optional arguments to tweak what things are interactive (e.g., do we want to include filtering? Searching? etc). This tutorial provides a decent explanation: https://posit.co/blog/new-in-gt-0-9-0-interactive-tables/. Here is the default interactivity, which includes sorting by columns (click the column headers) and splitting into different pages.

dat |> 
  gt() |> 
  opt_interactive()

9.2.3 Interactive table, extra options

Here we change the defaults on interactivity to add searching, filters, resizing columns, highlighting the row the mouse hovers over, and choosing the number of rows displayed at a time.

dat |> 
  gt() |> 
  opt_interactive(
    use_search = TRUE,
    use_filters = TRUE,
    use_resizers = TRUE,
    use_highlight = TRUE,
    use_page_size_select = TRUE
  )

9.3 Figures

ggplot figures can be made interactive by calling ggplotly() from the plotly library on a ggplot object. Again we will demonstrate this with mtcars, plotting the displacement against gross horsepower, coloring by the number of cylinders.

9.3.1 Non-interactive figure

We’ll add some basic labeling and formatting, but this plot a relatively common example of using ggplot. For simplicity in the next step, we’ll assign our plot to an object, and view that object.

gp = ggplot(dat, aes(x = disp, y = hp, col = as.factor(cyl)))+
  geom_point(size = 3)+
  labs(col = "Cylinders")+
  xlab("displacement (cubic in.)")+
  ylab("horsepower")+
  theme_bw(base_size = 14)
gp

9.3.2 Interactive figure

While the static figure is fine, it can be hard to distinguish points that are close together, and we can’t get precise values for individual points. We can make the figure interactive just by feeding it into ggplotly. This allows us to see details of points when we hover over them, as well as zoom in or out, and handle some other convenience features.

ggplotly(gp)

Plotly looks like it provides a rich suite of additional features for interactivity, but most of those features are beyond my current understanding. However, one incredibly powerful tool for clarifying figures is to change the hover-over tooltip to include additional information. For example, here we might want the hover to identify the individual cars.

Conveniently, this is easy to do with ggplotly. First, we add additional arguments in our aes call using arbitrary names. These arguments should include any additional information we want included in the hover-over. Here, I add a text argument which holds the car information. By default, ggplotly includes all aes terms in the hover-over, even if they’re not actually used in the plot otherwise.

gp = ggplot(dat, aes(x = disp, y = hp, col = as.factor(cyl),
                     text = car))+
  geom_point(size = 3)+
  labs(col = "Cylinders")+
  xlab("displacement (cubic in.)")+
  ylab("horsepower")+
  theme_bw(base_size = 14)
ggplotly(gp)

If we want the hover text to include only a subset of the terms in aes we can use the tooltip argument in ggplotly. We specify the hover text using the variable names in our aes call, not the variables of our dataframe.

gp = ggplot(dat, aes(x = disp, y = hp, col = as.factor(cyl), text = car))+
  geom_point(size = 3)+
  labs(col = "Cylinders")+
  xlab("displacement (cubic in.)")+
  ylab("horsepower")+
  theme_bw(base_size = 14)
ggplotly(gp, tooltip = c("text", "x", "y"))

Note that there is a long-standing bug (still present as of 6/28/24) in which the order of text in the hover-over cannot be controlled with ggplotly as its supposed to be. There is a workaround described here: https://github.com/plotly/plotly.R/issues/849.

9.4 trelliscopejs: interactive facet_wrap

We sometimes want to visualize plots for each fishery or each stock (or other situations where we want sub-plots representing different categories). The ggplot function facet_wrap is a fantastic tool for this kind of problem, but becomes less useful when the number of subplots reaches the point that they’re hard to visualize on a single screen. We’ll use the life expectancy data from the gapminder package to illustrate this.

9.4.1 Single plot

As a starting point let’s look at a single panel of our eventual plot: visualizing the US life expectancy through time. We’ll add some basic labels and theming, but keep the figure simple.

ggplot(gapminder |> filter(country == "United States"),
       aes(x = year, y = lifeExp))+
  geom_path()+
  xlab("Year")+
  ylab("Life expenctancy")+
  ggtitle("USA")+
  theme_bw(base_size = 14)

9.4.2 facet_wrap working well

What if we want to look at countries in continental North and Central America? One option is to use a single plot, with different colors for each country

countries.interest = c("United States", "Canada", "Mexico",
                       "Guatemala", "Honduras", "El Salvador",
                       "Nicaragua", "Costa Rica", "Panama")
ggplot(gapminder |> filter(country %in% countries.interest),
       aes(x = year, y = lifeExp, col = country))+
  geom_path()+
  xlab("Year")+
  ylab("Life expenctancy")+
  ggtitle("North and Central America")+
  theme_bw(base_size = 14)

This is nice for looking at overall trends, but it can be hard to focus on individual countries. We can use facet_wrap instead of coloring by country to break this up.

ggplot(gapminder |> filter(country %in% countries.interest),
       aes(x = year, y = lifeExp))+
  geom_path()+
  facet_wrap(~ country)+
  xlab("Year")+
  ylab("Life expenctancy")+
  ggtitle("North and Central America")+
  theme_bw(base_size = 13)

Now we can easily focus on patterns of individual countries. For example, we can see the dip in El Salvador in the 70s and 80s, which may reflect conditions in the lead up to and during the Salvadoran Civil War (1979-1992). We could not easily distinguish details like this by overlaying colored lines.

facet_wrap is often my go-to solution for plotting trends across fisheries or stock.

9.4.3 facet_wrap breaking down

But if we want to view facets for every country, we don’t really have room.

ggplot(gapminder,
       aes(x = year, y = lifeExp))+
  geom_path()+
  facet_wrap(~ country)+
  xlab("Year")+
  ylab("Life expenctancy")+
  ggtitle("All countries")+
  theme_bw(base_size = 14)

One option is to break the full data set into meaningful chunks, and plot each one as a separate faceted figure. Here we might make a plot for each continent. However, there is another option!

9.4.4 trelliscopejs for interactivity

With trelliscopejs, we can replace facet_wrap with facet_trelliscope. This takes a little while to run, but creates an incredibly flexible dashboard. In grid users can decide how many panels to show at once; in sort users can choose to sort by different criteria (for example, continent and then country). Perhaps most powerfully, users can use the filter section to specify individual countries to show, allowing users to make side-by-side comparisons of any set of countries they desire.

ggplot(gapminder,
       aes(x = year, y = lifeExp))+
  geom_path()+
  facet_trelliscope(~ country, path = '.', self_contained = TRUE)+
  xlab("Year")+
  ylab("Life expenctancy")+
  ggtitle("")+
  theme_bw(base_size = 14)
using data from the first layer

9.4.4.1 Including in a quarto or Rmarkdown file.

Note that when embedding a trelliscopejs figure in quarto or rmarkdown, it appears that the default path argument (NULL) causes an error. Setting path = '.' fixes this (as I did above).

Additionally, the default settings lead to an html document that relies on additional files which are created in the same folder as the .html file. Sharing the html without the additional files means the trelliscope visualization will not work. To create a stand-alone html file, we need to add the argument self_contained = TRUE to our facet_trelliscope call.

9.5 BONUS: Sparkline and friends from gtExtras

With some fiddling, we can actually include tiny figures within tables using the gtExtras package. Here we use that for the first gapminder. This is just a quick demo – see the details here: https://jthomasmock.github.io/gtExtras/articles/plotting-with-gtExtras.html. Here we’ll plot change in life expectancy over time and the general distribution of population size for each country within the period covered in the data.

dat.gm = gapminder |> 
  dplyr::arrange(year) |> 
  group_by(continent, country) |> 
  summarize(continent = continent[1],
            country = country[1],
            mean_gdp_percapita = mean(gdpPercap),
            lifeExp = list(lifeExp),
            dist_of_pop_size = list(pop)
  )
`summarise()` has grouped output by 'continent'. You can override using the
`.groups` argument.
gt(head(dat.gm, 20)) |> 
  gt_plt_sparkline(lifeExp) |> 
  gt_plt_dist(dist_of_pop_size, type = "density") |> 
  fmt_number(mean_gdp_percapita, decimals = 0) |> 
  opt_interactive()

Note that the lineplot for lifeExp is just showing the change sequentially – this would not work as intended if our data were not sorted by year, or if we had gaps in the years. There are a plethora of additional in-table plot options, including histograms and barplots.